fix: prevent SSRF in image decoder URL fetch - #4294
Conversation
Add _is_safe_url() to validate URLs against private/loopback/metadata IP ranges before fetching, preventing SSRF attacks via dataset image URLs. Fixes pytorch#4293
|
Hi @truongsontung! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
@claude review this PR |
|
Claude finished @tianyu-l's task in 1m 31s —— View job Code Review: SSRF protection in image decoderThanks for the fix @truongsontung. The intent is right and the structure is clean, but as written the check has a few bypasses that mean it doesn't actually stop a determined SSRF. Details below. 🔴 Blocking issues1. DNS rebinding / TOCTOU — the check is bypassable ( The robust fix is to resolve once, verify the resolved IP, and then connect to that pinned IP (passing the original 2. Redirects are not re-validated ( 3. IPv6 and IPv4-mapped addresses are not covered (
Rather than maintaining a hand-rolled list, lean on def _ip_is_blocked(ip: ipaddress._BaseAddress) -> bool:
mapped = getattr(ip, "ipv4_mapped", None)
if mapped is not None:
ip = mapped # unwrap ::ffff:a.b.c.d before classifying
return (
ip.is_private or ip.is_loopback or ip.is_link_local
or ip.is_reserved or ip.is_multicast or ip.is_unspecified
)This also picks up ranges the current list misses, e.g. 🟡 Suggestions
✅ What's good
Net: the approach is reasonable but the DNS-rebinding and redirect gaps make it bypassable today, and IPv6/mapped addresses are uncovered. I'd address 1-3 before merge. (Note: CLA still needs signing per the bot comment above before this can merge.) |
Address review feedback: - Use ipaddress.is_private/is_loopback/is_link_local/is_multicast/is_unspecified (covers IPv6) - Add _SSRFProtectedAdapter to block redirects to unsafe IPs - Resolve all IP addresses and check each Fixes pytorch#4293
|
@claude please review again |
|
Claude finished @tianyu-l's task in 4m 14s —— View job Re-review: SSRF protection in image decoderThanks for the follow-up @truongsontung. Good progress since the last round — issue #3 (IPv6) is substantially addressed. But one of the two blocking items is only apparently fixed: the redirect guard is wired to a hook 🔴 Blocking1. The redirect guard is dead code — redirects are still unvalidated (
Two clean ways to actually close it: # Option A: subclass Session (resolve_redirects is a Session method)
class _SSRFProtectedSession(requests.Session):
def resolve_redirects(self, resp, req, **kwargs):
loc = resp.headers.get("Location")
if loc and not _is_safe_url(urljoin(resp.url, loc)):
raise requests.exceptions.InvalidURL(f"Blocked redirect: {loc}")
return super().resolve_redirects(resp, req, **kwargs)# Option B: disable redirects and loop manually, re-validating each hop
resp = session.get(image, timeout=10, allow_redirects=False)
while resp.is_redirect:
nxt = urljoin(resp.url, resp.headers["Location"])
if not _is_safe_url(nxt):
raise ValueError(f"Blocked redirect: {nxt}")
resp = session.get(nxt, timeout=10, allow_redirects=False)Note both still resolve the redirect target's 🟡 Still open from last review2. DNS rebinding / TOCTOU ( 3. No test added. Per 🟢 Minor
✅ Improved since last round
Net: the IPv6 hardening landed well, but the redirect protection is currently a no-op because it hooks the wrong class — that's the one I'd block on. DNS-rebinding and a test are the remaining follow-ups. (CLA still needs signing per the bot comment above before this can merge.) Note: I couldn't execute |
…is on Session, not HTTPAdapter) Address Claude re-review feedback: _SSRFProtectedAdapter was dead code because resolve_redirects is a Session method, not HTTPAdapter. Switched to _SSRFProtectedSession subclass that intercepts redirects.
|
@claude review again |
|
Claude finished @tianyu-l's task in 3m 37s —— View job Re-review: SSRF protection in image decoderThanks for the update @truongsontung. The dead-code problem from last round is genuinely fixed now — but the redirect guard as written only validates the first hop, so a multi-hop redirect chain still slips through. Details below. 🟢 Resolved since last roundRedirect hook now actually fires ( 🔴 Blocking — only the first redirect hop is validated (
|
Claude review feedback: previous override only checked first redirect hop. Rewrote resolve_redirects to manually loop each hop with _is_safe_url() validation, preventing multi-hop redirect bypass.
Summary
Add SSRF protection to the image decoder URL fetch in _decode_image.
Change
Added _is_safe_url() helper that validates URLs against private/loopback/metadata IP ranges before fetching. This prevents SSRF attacks via dataset image URLs.
Fix
In torchtitan/hf_datasets/multimodal/utils/image.py:36:
Fixes #4293